package velox.jcr.mapper;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import net.sf.cglib.beans.ImmutableBean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springmodules.jcr.SessionFactory;
import org.springmodules.jcr.SessionFactoryUtils;

import velox.jcr.mapper.LazyBeanFactory.LazyBean;
import velox.jcr.util.PathUtils;


public class MapperManagerImpl implements MapperManager {
	private Logger logger = LoggerFactory.getLogger(MapperManagerImpl.class);
	
	private List<MapperDefinition<?>> mapperDefinitions; 
	private SessionFactory sessionFactory;
	private NestedThreadLocalSet<Set<Object>> persistClosure = new NestedThreadLocalSet<Set<Object>>();
	
	public MapperManagerImpl(SessionFactory sessionFactory, SecondLevelCache secondLevelCache) {
		this(sessionFactory, secondLevelCache, Filter.NULL);
	}
	
	public MapperManagerImpl(SessionFactory sessionFactory, SecondLevelCache secondLevelCache, Filter filter) {
		logger.info("Creating MapperManagerImpl");
		
		this.sessionFactory = sessionFactory;
		this.mapperDefinitions = new ArrayList<MapperDefinition<?>>();
		
		UnitOfWorkHolder.initialize(secondLevelCache, this, filter);
	}
	
	public void setMapperDefinitions(List<MapperDefinition<?>> mapperDefinitions) {
		for (MapperDefinition<?> mapperDefinition : mapperDefinitions) {
			addMapperDefinition(mapperDefinition);
		}
	}
	
	public <T> T get(NodeRef nodeReference, Class<T> type) throws RepositoryException {
		// TODO: We should try to cache any discriminator mappers to avoid using the node
		Mapper<T> m = getMapper(type, nodeReference);

		T t = UnitOfWorkHolder.get().get(nodeReference.getUUID(), m);
		if (t != null) {
			return applyFilter(t, type);
		}

		Node n = nodeReference.getNode();
		logger.debug("Reading node {}", n.getUUID());
		T o = m.read(n);
		if (isReadOnly()) {
			o = makeImmutable(o);
		}
		UnitOfWorkHolder.get().registerLoad(nodeReference.getUUID(), o, m);
		return applyFilter(o, type);	
	}

	public void addMapperDefinition(MapperDefinition<?> mapperDefinition) {
		logger.debug("Registering mapper definition {}", mapperDefinition);
		if (mapperDefinition.getDiscriminator() != null) {
			boolean superClassFound = false;
			for (MapperDefinition<?> def : mapperDefinitions) {
				if (def.getEntityClass().equals(mapperDefinition.getParentEntityClass())) {
					def.setSuperclass(true);
					superClassFound = true;
				}
			}
			
			if (! superClassFound) {
				throw new IllegalArgumentException(String.format("Cannot add sub class of %s until definition of %s exists", 
						mapperDefinition.getParentEntityClass().getSimpleName(), mapperDefinition.getParentEntityClass().getSimpleName()));
			}
		}
		this.mapperDefinitions.add(mapperDefinition);
	}	

	public <T> void remove(T t) throws RepositoryException {
		if (isReadOnly()) 
			throw new IllegalStateException("Cannot remove in read-only transaction");

		Mapper<T> m = getMapper(t, null);
		NodeRef n = new NodeRef(m.getId(t));
		n.getNode().remove();
		UnitOfWorkHolder.get().registerRemove(n.getUUID());
	}

	public <T> T persist(NodeRef parent, String name, T t) throws RepositoryException {
		if (isReadOnly()) 
			throw new IllegalStateException("Cannot persist in read-only transaction");

		Set<Object> s = persistClosure.enter();
		try {
			if (s.contains(t)) return t;
			s.add(t);
			
			Node n = parent.getNode().addNode(name);
			n.addMixin("mix:referenceable");
	
			Mapper<T> m = getMapper(t, null);
			m.write(t, n, true);
			
			UnitOfWorkHolder.get().registerPersist(n.getUUID(), t);
		} finally {
			persistClosure.exit();
		}
		
		return t;
	}

	public <T> T update(T t) throws RepositoryException {
		if (isReadOnly()) 
			throw new IllegalStateException("Cannot update in read-only transaction");

		Set<Object> s = persistClosure.enter();
		try {
			if (s.contains(t)) return t;
			s.add(t);
	
			Mapper<T> m = getMapper(t, null);
			NodeRef n = new NodeRef(m.getId(t));
			m.write(t, n.getNode(), false);
			
			// Move node to new path if it has been renamed
			String name = m.getName(t);
			if (name != null && ! name.equals(n.getNode().getName())) {
				getSession().move(n.getNode().getPath(),
						PathUtils.join(n.getNode().getParent().getPath(), m.getName(t)));
			}
			
			UnitOfWorkHolder.get().registerUpdate(n.getUUID(), t);
		} finally {
			persistClosure.exit();
		}
		
		return t;
	}

	public void save() throws RepositoryException {
		if (isReadOnly()) 
			throw new IllegalStateException("Cannot save in read-only transaction");

		getSession().save();
	}

	public Session getSession() {
		return SessionFactoryUtils.getSession(this.sessionFactory, false);
	}
	
	@SuppressWarnings("unchecked")
	private <T> T makeImmutable(T o) {
		return (T) ImmutableBean.create(o);
	}
	
	private <T> T applyFilter(T o, Class<T> type) {
		boolean result = UnitOfWorkHolder.get().getFilter().include(o);
		if (! result) throw new AccessDeniedException("Access to object " + o + " is denied");
		return o;
	}
	
	private boolean isReadOnly() {
		return TransactionSynchronizationManager.isCurrentTransactionReadOnly();
	}

	@SuppressWarnings("unchecked")
	public <T> Mapper<T> getMapper(T t, NodeRef nodeReference) throws RepositoryException {
		return getMapper((Class<? super T>) t.getClass(), nodeReference);
	}
	
	@SuppressWarnings("unchecked")
	public <T> Mapper<T> getMapper(Class<? super T> type, NodeRef nodeReference) throws RepositoryException {
		if (LazyBean.class.isAssignableFrom(type)) {
			logger.debug("Found lazy bean, using superclass {} instead", type.getSuperclass());
			type = type.getSuperclass();
		}
		
		Mapper<T> m = null;
		MapperDefinition<?> mapperDefinition = null;
		for (MapperDefinition<?> def : mapperDefinitions) {
			if (def.getEntityClass().equals(type)) {
				m = (Mapper<T>) def.getMapper();
				mapperDefinition = def;
			}
		}

		// TODO: We should somehow cache this lookup
		if (nodeReference != null && mapperDefinition.isSuperclass()) {
			Node n = nodeReference.getNode();
			if (m.getDiscriminator(n) != null) {
				String d = m.getDiscriminator(n);
				for (MapperDefinition<?> def : mapperDefinitions) {
					if (def.getParentEntityClass().equals(type) && d.equals(def.getDiscriminator())) {
						return getMapper((Class<? super T>) def.getEntityClass(), nodeReference);
					}
				}
			}
		}

		if (m == null) {
			logger.error("Cannot find mapper for {}", type);
		}
		return m;
	}
}
